/*
 * Alloy Analyzer
 * Copyright (c) 2002 Massachusetts Institute of Technology
 *
 * The Alloy Analyzer is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * The Alloy Analyzer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with the Alloy Analyzer; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package alloy.util;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.GraphicsEnvironment;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Vector;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BoxLayout;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.SwingConstants;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;

import alloy.gui.AlloySwingUtilities;

/**
 * Provides a GUI for editing parameters.
 */
public class ParamEditor extends JTabbedPane {

    //    static final String[] fontFaces = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
    //static final Font[] fontFaces = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts();
    static Font[] fontFaces = null; // popuplated lazily
    static final String[] fontSizes = {"6","8","9","10","11","12","14","18","24",
                       "28", "36", "48", "56", "64", "72"};


    /**
     * {@link Hashtable} filled out by this routine, mapping {@link String} group name
     * to a {@link Hashtable} mapping each {@link Parameter} name in that group to a
     * {@link JComponent} widget allocated to editing that parameter.  The widget
     * can be used to read the current value of the parameter.
     */
    private Hashtable _grp2param2widget;

    /**
     * {@link Params} we are editing.
     */
    private Params _params;

    /**
     * {@link Hashtable} mapping each group name to a
     * {@link Hashtable} mapping each {@link Parameter} name in that group
     * to its value as of the last call to updateParams()
     */
    private Hashtable _grp2param2value;

    /**
     * (@link Action} to perform whenever a parameter value is changed.
     */
    private Action _paramChangeAction;

      /////////////
     // Methods //
    /////////////

    /**
     * Construct a tabbed pane for editing parameters.  Each group of parameters
     * is allocated a separate tabbed pane.
     * @param solvers {@link Hashtable} from {@link String} group name to a {@link Vector}
     *                of {@link Parameter}s in that group.
     * @param s2w {@link Hashtable} filled out by this routine, mapping {@link String} group name
     *            to a {@link Hashtable} mapping each {@link Parameter} name in that group to a
     *            {@link JComponent} widget allocated to editing that parameter.  The widget
     *            can be used to read the current value of the parameter.
     */
    public ParamEditor(Params params_) {
    this(params_, null);
    }
    public ParamEditor(Params params_, Action paramChangeAction_) {
    _params = params_;
    _paramChangeAction = paramChangeAction_;
    _grp2param2widget = new Hashtable();
    _grp2param2value = new Hashtable();
    updateEditor();
    }

    private void doParamChangeAction() {
    if (_paramChangeAction != null)
        _paramChangeAction.actionPerformed(null);
    }

    /**
     * Update this ParamEditor with the current values from the {@link Params} structure being edited
     */
    public void updateEditor() {
    removeAll();
    _grp2param2widget.clear();

    int tabNum=0;
    for (Iterator grpNameIter = _params.getGroupNames().iterator(); grpNameIter.hasNext();tabNum++) {
        String grpName = (String) grpNameIter.next();
        Hashtable h = new Hashtable();
        Hashtable h2 = new Hashtable();
        _grp2param2widget.put(grpName,h);
        _grp2param2value.put(grpName,h2);
        add(_params.getGroupDescr(grpName),
        makePanelForParamGroup(_params.getGroupParams(grpName), h, h2));
        setToolTipTextAt(tabNum, grpName);
    }
    }

    /**
     * Permanently update using current values.
     */
    public void updateParams() {
    for (Iterator grpNameIter = _params.getGroupNames().iterator(); grpNameIter.hasNext();) {
        String grpName = (String) grpNameIter.next();
        Vector grpParams = _params.getGroupParams(grpName);
        Hashtable param2widget = (Hashtable)_grp2param2widget.get(grpName);
        Hashtable param2value = (Hashtable)_grp2param2value.get(grpName);
        for (Iterator paramIter = grpParams.iterator(); paramIter.hasNext();) {
        Parameter param = (Parameter)paramIter.next();
        param2value.put(param.getName(), param.getValue());
        }
    }
    }

    /**
     * Restore the {@link Params} structure being edited to the values at the
     * last call to updateParams() or, if updateParams() was never called, to
     * the initial values when the GUI was created.
     */
    public void restoreParams() {
    for (Iterator grpNameIter = _params.getGroupNames().iterator(); grpNameIter.hasNext();) {
        String grpName = (String) grpNameIter.next();
        Vector grpParams = _params.getGroupParams(grpName);
        Hashtable param2widget = (Hashtable)_grp2param2widget.get(grpName);
        Hashtable param2value = (Hashtable)_grp2param2value.get(grpName);
        for (Iterator paramIter = grpParams.iterator(); paramIter.hasNext();) {
        Parameter param = (Parameter)paramIter.next();
        JComponent jc = (JComponent)param2widget.get(param.getName());
        String value = (String)param2value.get(param.getName());
        if (jc instanceof JCheckBox)
            ((JCheckBox)jc).setSelected(Integer.parseInt(value) == 1);

        else if (jc instanceof JComboBox)
            ((JComboBox)jc).setSelectedItem(value);
        else if (jc instanceof JTextField)
            ((JTextField)jc).setText(value);
        else if (jc instanceof FontWidget)
            ((FontWidget)jc).setFontStringValue(value);
        param.setValue(value);
        }
    }
    doParamChangeAction();
    }

    /**
     * Construct a panel for editing the given parameters.
     * @param params {@link Vector} of {@link Parameter}
     * @param p2w {@link Hashtable} to be filled with parameter-to-widget mappings.
     * @param p2v {@link Hashtable} to be filled with parameter-to-value mappings.
     */
    private JPanel makePanelForParamGroup(Vector /* of Parameter */ params, Hashtable p2w, Hashtable p2v/*, String grpDescr_*/) {
    JPanel panel = new JPanel();

    GridBagLayout gbl = new GridBagLayout();
    GridBagConstraints gbc = new GridBagConstraints();
    gbc.anchor = GridBagConstraints.WEST;
    gbc.fill = GridBagConstraints.HORIZONTAL;
    gbc.insets = new Insets(0,10,0,10);

    panel.setLayout(gbl);
    panel.setAlignmentY(JPanel.TOP_ALIGNMENT);
    int row = 0;

    for (Enumeration e = params.elements(); e.hasMoreElements(); row++) {
        final Parameter p = (Parameter) e.nextElement();
        p2v.put(p.getName(), p.getValue());

        JLabel message = new JLabel(p.getMessage());
        message.setHorizontalAlignment(JLabel.RIGHT);
        message.setToolTipText(p.getName());
        if (AlloySwingUtilities.onMac())
        message.setFont(message.getFont().deriveFont((float)12));

        switch (p.getType()) {
        case Parameter.BOOLEAN:
        final JCheckBox cbox = new JCheckBox();
        cbox.setSelected(Integer.parseInt(p.getValue()) == 1);
        cbox.setHorizontalAlignment(SwingConstants.LEFT);
        cbox.setVerticalAlignment(SwingConstants.CENTER);
        cbox.setAction(AlloySwingUtilities.action(new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                p.setValue(cbox.getModel().isSelected() ? "1" : "0");
                doParamChangeAction();
            }
            }));
        p2w.put(p.getName(), cbox);
        grid_add(panel, cbox,gbl,gbc,.2,0,1,row,1,1);
        break;

        case Parameter.INT:
        case Parameter.DOUBLE:
        case Parameter.STRING:
        case Parameter.PATH:
        final JTextField jtf = new JTextField(p.getValue());
        jtf.addFocusListener((FocusListener)AlloySwingUtilities.listener(new FocusAdapter() {
            public void focusLost(FocusEvent e) {
                p.setValue(jtf.getText());
                doParamChangeAction();
            }
            }));
        jtf.setAction(AlloySwingUtilities.action(new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                p.setValue(jtf.getText());
                doParamChangeAction();
            }
            }));
        p2w.put(p.getName(), jtf);
        grid_add(panel, jtf ,gbl,gbc,.2,0,1,row,1,1);
        break;

        case Parameter.ENUM:
        final JComboBox jcb = new JComboBox(p.getEnum().values);
        jcb.setSelectedItem(p.getValue());
        jcb.setAction(AlloySwingUtilities.action(new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                p.setValue((String)jcb.getSelectedItem());
                doParamChangeAction();
            }
            }));
        p2w.put(p.getName(), jcb);
        grid_add(panel,jcb,gbl,gbc,.2,0,1,row,1,1);
        // NOTE: the descriptions for individual ENUM values
        //       are not currently being used anywhere
        break;
        case Parameter.FONT:
        final FontWidget fw = new FontWidget(p);
        fw.setAction(AlloySwingUtilities.action(new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                p.setValue(fw.getFontStringValue());
                doParamChangeAction();
            }
            }));
        p2w.put(p.getName(), fw);
        grid_add(panel,fw,gbl,gbc,.2,0,1,row,1,1);
        break;
        default:
        Dbg.fail("Unknown parameter type: " + p.getType());
        }

        grid_add(panel,message,gbl,gbc,.1,0,0,row,1,1);
        panel.setMinimumSize(new java.awt.Dimension(10,10));
    }
    // fill remaining vertical space
    grid_add(panel,new JPanel(),gbl,gbc,0,1,0,row,GridBagConstraints.REMAINDER,1);
    return panel;
    }
    public static void main (String [] args) throws IOException {
    ParamReader pr = new ParamReader();
    Params params = null;
    try {
        params = ParamReader.loadParams(System.in);
    } catch (ParamFormatException pfe_) {
        Dbg.fatal("Error parsing param file", pfe_);
    }

    //System.out.println("params=" + params);

    ParamEditor paramEditor = new ParamEditor(params);
    JButton button = new JButton("Print");
    button.addActionListener((ActionListener)AlloySwingUtilities.listener(new PrintListener(params, paramEditor)));
    JFrame f = new JFrame();
    f.getContentPane().setLayout(new BorderLayout());
    f.getContentPane().add("North",paramEditor);
    f.getContentPane().add("South",button);

    f.pack();
    f.show();
    }

    private static void grid_add (JComponent jc, Component c, GridBagLayout gbl,
                  GridBagConstraints gbc,
                  double weightx, double weighty, int x, int y, int w, int h) {
    gbc.weightx = weightx;
    gbc.weighty = weighty;
    gbc.gridx = x;
    gbc.gridy = y;
    gbc.gridwidth = w;
    gbc.gridheight = h;
    gbl.setConstraints (c, gbc);
    jc.add (c);
    }

    /*
    public String output_choices (String fname, Hashtable s2w, String solver, Hashtable s2p) {
    try {
        FileOutputStream fos = new FileOutputStream(fname);
        PrintWriter pw = new PrintWriter(fos);
        Hashtable p2w = (Hashtable) s2w.get(solver);
        Vector params = (Vector) s2p.get(solver);
        pw.println("GROUP "+solver);
        for (Enumeration e = params.elements(); e.hasMoreElements();) {
        Parameter param = (Parameter) e.nextElement();
        JComponent jc = (JComponent) p2w.get(param.getName());
        String setting = null;
        if (jc instanceof JCheckBox)
            setting = (((JCheckBox) jc).getModel().isSelected() ? "1" : "0")+"";

        if (jc instanceof JComboBox)
            setting = ((JComboBox) jc).getSelectedItem()+"";

        if (jc instanceof JTextField)
            setting = ((JTextField) jc).getText()+"";

        pw.print("PARAM "+param.getName());
        pw.print(" "+param.typeString()+" "+setting.trim());
        pw.println(" \""+param.message+"\"");
        }
        pw.println("ENDGROUP");

        pw.close();
        return solver;
    }
    catch (Exception e) {
        fatal(e);
    }

    }

    public String output_choices (StringWriter sw, Hashtable s2w, String solver, Hashtable s2p) {
    try {
        PrintWriter pw = new PrintWriter(sw);
        Hashtable p2w = (Hashtable) s2w.get(solver);
        Vector params = (Vector) s2p.get(solver);
        pw.println("GROUP "+solver);
        for (Enumeration e = params.elements(); e.hasMoreElements();) {
        Parameter param = (Parameter) e.nextElement();
        JComponent jc = (JComponent) p2w.get(param.getName());
        String setting = null;
        if (jc instanceof JCheckBox)
            setting = (((JCheckBox) jc).getModel().isSelected() ? "1" : "0")+"";

        if (jc instanceof JComboBox)
            setting = ((JComboBox) jc).getSelectedItem()+"";

        if (jc instanceof JTextField)
            setting = ((JTextField) jc).getText()+"";

        pw.print("PARAM "+param.getName());
        pw.print(" "+param.typeString()+" "+setting.trim());
        pw.println(" \""+param.message+"\"");
        }
        pw.println("ENDGROUP");

        pw.flush();
        return solver;
    }
    catch (Exception e) {
        fatal(e);
        return null;
    }

    }
    */

    class FontWidget extends JPanel {
    //Font originalFont;
    JComboBox faceBox;
    JComboBox sizeBox;
    final Parameter param;

    FontWidget(Parameter param_) {
        param = param_;
        setLayout(new BoxLayout(FontWidget.this, BoxLayout.X_AXIS));

        ActionListener fontChanger = new ActionListener() {
            public void actionPerformed(ActionEvent e) {
            param.setValue(getFontStringValue());
            doParamChangeAction();
            }
        };

        if (fontFaces==null) {
            faceBox = new JComboBox();
        } else {
            faceBox = new JComboBox(fontFaces);
        }
        
        faceBox.setRenderer(new FontFaceCellRenderer());
        faceBox.addActionListener((ActionListener)AlloySwingUtilities.listener(fontChanger));
        add(faceBox);
        
        faceBox.addPopupMenuListener(new PopupMenuListener() {
            public void popupMenuCanceled(PopupMenuEvent e) {}
            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {}
            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
                if (fontFaces == null) {                    
                    setCursor(new Cursor(Cursor.WAIT_CURSOR));                    
                    fontFaces = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts();                    
                    setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
                }

		int currentListSize = faceBox.getItemCount();
                if (currentListSize < fontFaces.length) {
		    String selectedFont = getFontStringValue();
                    //Font selectedFont = (Font) faceBox.getSelectedItem();
                    //faceBox.removeAllItems();
                    for (int i = 0 ; i < fontFaces.length ; i++) {
                        faceBox.addItem(fontFaces[i]);
                    }

		    // remove the items originally there
		    for (int i=0; i < currentListSize; i++) {
			faceBox.removeItemAt(i);
		    }

		    setFontStringValue(selectedFont);
                    //faceBox.setSelectedItem(selectedFont);
                }
            }
            });

        sizeBox = new JComboBox(fontSizes);
        sizeBox.addActionListener((ActionListener)AlloySwingUtilities.listener(fontChanger));
        add(sizeBox);

        // make it easy to add the default value at construction by setting editable to true
        // temporarily
        faceBox.setEditable(true);
        sizeBox.setEditable(true);
        setFontStringValue(param.getValue());
	if (AlloySwingUtilities.onMac()) {
	try {
	faceBox.addItem(Parameter.stringToFont(param.getValue()));
	} catch (FontFormatException e) {
	    Dbg.fail("Invalid font value during initialization");
	}
	}
        faceBox.setEditable(false);
        sizeBox.setEditable(false);
    }

    public void setAction(Action a) {
        faceBox.setAction(a);
        sizeBox.setAction(a);
    }

    void setFontValue(Font font) {
        faceBox.setSelectedItem(font.deriveFont((float)1));
        sizeBox.setSelectedItem(""+font.getSize());
    }

    Font getFontValue() {
        return ((Font)faceBox.getSelectedItem()).deriveFont(Float.parseFloat(sizeBox.getSelectedItem().toString()));
    }

    void setFontStringValue(String s) {
        try {
        setFontValue(Parameter.stringToFont(s));
        } catch (FontFormatException e) {
        Dbg.fail("Invalid font value");
        }
    }

    String getFontStringValue() {
        return Parameter.fontToString(getFontValue());
    }
    }
}

class FontFaceCellRenderer implements ListCellRenderer
{
    DefaultListCellRenderer defaultRenderer;

    public FontFaceCellRenderer () {
    defaultRenderer = new DefaultListCellRenderer ();
    }

    public Component getListCellRendererComponent (JList list,
                           Object value,
                           int index,
                           boolean isSelected,
                           boolean cellHasFocus)
    {
    Font font = (Font)value;
    return defaultRenderer.getListCellRendererComponent (list,
                                 font.getFontName(),
                                 index,
                                 isSelected,
                                 cellHasFocus);
    }
}

class PrintListener implements ActionListener {

    private Params _params;
    private ParamEditor _paramEditor;

    public PrintListener (Params params_, ParamEditor paramEditor_) {
        _params = params_;
        _paramEditor = paramEditor_;
    }

    public void actionPerformed (ActionEvent ev) {
        _paramEditor.updateParams();
        // System.out.println(_params);
    }
}
/*
class ParamListener implements ActionListener {
    public Parameter p;
    public JComboBox jcb;
    public JTextField message;
    public ParamListener (Parameter pa, JComboBox j, JTextField m) {
    p = pa;
    jcb = j;
    message = m;
    }
    public void actionPerformed (ActionEvent e) {
    String text = (String)p.enum.v2d.get(jcb.getSelectedItem());
    // System.out.println("setting text: "+ text);
    message.setText(p.message+" ("+text+")");
    }
}
*/

